Skip to content

✨ Add pass and patterns for Classical Control Replacement#1756

Open
DRovara wants to merge 40 commits into
mainfrom
mlir/replace-classical-controls
Open

✨ Add pass and patterns for Classical Control Replacement#1756
DRovara wants to merge 40 commits into
mainfrom
mlir/replace-classical-controls

Conversation

@DRovara

@DRovara DRovara commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

Description

Part 2 out of 3 of the Qubit Reuse Implementation

This PR ports the previous implementation of the Classical Control Replacement pass to the new dialect infrastructure.

This path has the goal of replacing controlled gates with "classically controlled gates" whenever a classical value for the control qubit is available because it has just been measured.
In those cases, we remove the control from the CtrlOp and instead introduce a qco.ifOp.

This is one of 3 PRs that will, in total, constitute qubit reuse:

  1. Measurement Lifitng
  2. Replacing Classical Controls
  3. Reuse Qubits

Requires #1705 to be merged first.

AI Notice: Gemini 3.1 and Claude Sonnet 4.6 were used for the generation of some code.

Checklist

  • The pull request only contains commits that are focused and relevant to this change.
  • I have added appropriate tests that cover the new/changed functionality.
  • I have updated the documentation to reflect these changes.
  • I have added entries to the changelog for any noteworthy additions, changes, fixes, or removals.
  • I have added migration instructions to the upgrade guide (if needed).
  • The changes follow the project's style guidelines and introduce no new warnings.
  • The changes are fully tested and pass the CI checks.
  • I have reviewed my own code changes.

If PR contains AI-assisted content:

  • I have disclosed the use of AI tools in the PR description as per our AI Usage Guidelines.
  • AI-assisted commits include an Assisted-by: [Model Name] via [Tool Name] footer.
  • I confirm that I have personally reviewed and understood all AI-generated content, and accept full responsibility for it.

@DRovara DRovara self-assigned this Jun 2, 2026
@DRovara DRovara added feature New feature or request c++ Anything related to C++ code MLIR Anything related to MLIR labels Jun 2, 2026
@DRovara DRovara added this to the MLIR Support milestone Jun 2, 2026
@DRovara

DRovara commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit full review

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Full review triggered.

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3263c5bd-b189-4e86-8aa1-c4c4e583ca27

📥 Commits

Reviewing files that changed from the base of the PR and between 70ea613 and 0e9dd2f.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • mlir/include/mlir/Dialect/QCO/Transforms/Passes.td
  • mlir/lib/Dialect/QCO/Transforms/Optimizations/ReplaceClassicalControls.cpp
  • mlir/unittests/Dialect/QCO/Transforms/Optimizations/CMakeLists.txt
  • mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_replace_classical_controls.cpp

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added a compiler optimization pass that converts measurement-controlled operations into structured conditional operations.
    • Supports combined measurement conditions and optimized handling of diagonal phase operations.
  • Tests

    • Added comprehensive coverage for single- and multi-control scenarios, conditional combinations, and optimization cases.
  • Documentation

    • Updated the unreleased changelog with the related contribution reference.

Walkthrough

Adds the ReplaceClassicalControls QCO pass, which rewrites measurement-derived controls into qco.if operations, supports diagonal-control swapping, wires the optimization into builds, and adds eight structural equivalence tests.

Changes

QCO Classical-Control Replacement

Layer / File(s) Summary
Pass contract and build integration
mlir/include/mlir/Dialect/QCO/Transforms/Passes.td, mlir/unittests/.../CMakeLists.txt, CHANGELOG.md
Declares the pass, adds its tests to the optimization target, and records the pull request reference.
Classical-control rewrite implementation
mlir/lib/Dialect/QCO/Transforms/Optimizations/ReplaceClassicalControls.cpp
Detects predecessor measurement outcomes, builds conjunction conditions and qco.if regions, rewires SSA results, and applies diagonal-control swapping where applicable.
Classical-control replacement validation
mlir/unittests/.../test_qco_replace_classical_controls.cpp
Tests single and multiple measured controls, conjunctions, and diagonal swapping scenarios against reference modules.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CtrlOp
  participant RewritePattern
  participant MeasureOp
  participant IfOp
  CtrlOp->>RewritePattern: inspect controls and targets
  RewritePattern->>MeasureOp: retrieve predecessor outcomes
  RewritePattern->>IfOp: create condition and nested CtrlOp
  IfOp-->>CtrlOp: replace SSA outputs
Loading

Possibly related PRs

Suggested reviewers: burgholzer

Poem

A rabbit hops through measured light,
Turning controls to branches right.
Bits conjoin, the qubits flow,
Diagonal swaps perform their show.
Tests bloom green in MLIR rows—
Carrots for every rewrite pose!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding the classical control replacement pass and patterns.
Description check ✅ Passed The description includes the change summary, motivation, dependency on PR #1705, and the required checklist/AI disclosure.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch mlir/replace-classical-controls

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
mlir/include/mlir/Dialect/QCO/IR/QCOOps.td (1)

1071-1077: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Drop Pure from qco.ctrl, qco.inv, and qco.if (region wrappers).

Pure in MLIR expands to AlwaysSpeculatable + NoMemoryEffect (via AlwaysSpeculatableImplTrait), which asserts speculatability and no memory effects. These wrapper ops already model their effects via RecursiveMemoryEffects, so keeping Pure overstates effects independently of what’s inside the nested region. Keep RecursiveMemoryEffects, but remove Pure from CtrlOp, InvOp, and IfOp (incl. ranges covering the corresponding defs around 1071-1077, 1150-1154, and 1223-1232).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mlir/include/mlir/Dialect/QCO/IR/QCOOps.td` around lines 1071 - 1077, The QCO
region wrapper ops declare the Pure trait which expands to AlwaysSpeculatable +
NoMemoryEffect and incorrectly duplicates/overrides the region-modelled effects;
remove the Pure trait from the op definitions for CtrlOp, InvOp, and IfOp (the
TD defs named CtrlOp, InvOp, IfOp) so they only keep RecursiveMemoryEffects (and
other existing traits like UnitaryOpInterface, AttrSizedOperandSegments,
AttrSizedResultSegments, SameOperandsAndResultType, SameOperandsAndResultShape,
SingleBlockImplicitTerminator) to ensure effects are solely driven by the nested
region.
mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp (1)

1105-1165: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Enforce the configured main return signature before finalizing.

After initialize(TypeRange), the no-arg finalize() still hardcodes an i64 zero, and finalize(ValueRange) never checks arity/types against main. initialize({}) or any non-i64 signature now silently builds an invalid func.return.

Suggested fix
 OwningOpRef<ModuleOp> QCOProgramBuilder::finalize() {
   checkFinalized();
-
-  auto exitCode = intConstant(0);
-  return finalize({exitCode});
+  func::FuncOp mainFunc = nullptr;
+  for (auto op : cast<ModuleOp>(module).getOps<func::FuncOp>()) {
+    if (op.getName() == "main") {
+      mainFunc = op;
+      break;
+    }
+  }
+  if (!mainFunc) {
+    llvm::reportFatalUsageError("Could not find main function");
+  }
+
+  auto resultTypes = mainFunc.getFunctionType().getResults();
+  if (resultTypes.empty()) {
+    return finalize({});
+  }
+  if (resultTypes.size() == 1 && resultTypes.front().isSignlessInteger(64)) {
+    auto exitCode = intConstant(0);
+    return finalize({exitCode});
+  }
+  llvm::reportFatalUsageError(
+      "Use finalize(returnValues) when main has custom return types");
 }
 
 OwningOpRef<ModuleOp> QCOProgramBuilder::finalize(ValueRange returnValues) {
   checkFinalized();
@@
   if (!mainFunc) {
     llvm::reportFatalUsageError("Could not find main function");
   }
+
+  auto resultTypes = mainFunc.getFunctionType().getResults();
+  if (returnValues.size() != resultTypes.size()) {
+    llvm::reportFatalUsageError(
+        "Return values must match the main function return arity");
+  }
+  for (auto [value, type] : llvm::zip_equal(returnValues, resultTypes)) {
+    if (value.getType() != type) {
+      llvm::reportFatalUsageError(
+          "Return values must match the main function return types");
+    }
+  }
@@
   func::ReturnOp::create(*this, returnValues);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp` around lines 1105 - 1165,
The finalize paths currently emit a func::ReturnOp without validating or
matching the main function's declared result types (initialize(TypeRange) may
set a non-i64 or different arity), so update QCOProgramBuilder::finalize() and
QCOProgramBuilder::finalize(ValueRange) to first query
mainFunc.getType().getResults(), assert that returnValues.size() equals that
result list size and that each returnValues[i].getType() == expectedResults[i],
and if the no-arg finalize() is used produce appropriate default constants per
expectedResults type (not always an i64 zero) instead of unconditionally calling
intConstant(0); then create the func::ReturnOp with validated/matched values
(and emit a clear error via llvm::reportFatalUsageError if arity/types
mismatch). Ensure references: finalize(), finalize(ValueRange),
initialize(TypeRange), func::ReturnOp::create, mainFunc.getType().getResults(),
and intConstant are used when implementing the checks and per-type default
constant construction.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Line 14: Replace the incorrect changelog bullet "✨ Add Dead Gate Elimination
Pattern ([`#1755`]) ([**`@DRovara`**])" with an entry that references this PR
(`#1756`) and accurately describes the implemented feature (measurement lifting /
classical-control replacement), e.g., update the PR number to 1756 and change
the description to mention "Measurement lifting and classical-control
replacement" so the released notes match the actual code changes.

In `@mlir/include/mlir/Dialect/QCO/QCOUtils.h`:
- Around line 240-276: The header's inline function checkAndRemoveDeadGate uses
QCO-specific types (SinkOp, UnitaryOpInterface, MeasureOp, IfOp, Reset) but
doesn't include their declarations, making the header order-dependent; fix by
making the header self-contained: add a direct include of the QCO ops/declaring
header (e.g., include "mlir/Dialect/QCO/QCOOps.h") at the top of
mlir/include/mlir/Dialect/QCO/QCOUtils.h so dyn_cast/isa and method calls
resolve, or alternatively move the implementation of checkAndRemoveDeadGate out
of the header into a .cpp that includes QCOOps.h if you prefer to avoid
additional header dependencies.

In `@mlir/lib/Dialect/QCO/Transforms/Optimizations/ReplaceClassicalControls.cpp`:
- Around line 143-155: The lambda passed to IfOp::create currently always builds
a CtrlOp (via CtrlOp::create) even when newControls (derived from toKeep.size())
is empty, which yields invalid IR because qco::CtrlOp requires >=1 control;
modify the lambda to branch on newControls.empty(): if non-empty, keep the
existing flow (create CtrlOp, inline newCtrl.getRegion() into op.getRegion(),
return newCtrl.getOutputQubits()); if empty, instead directly clone or move the
original unitary body into the IfOp region (use rewriter to inline/clone the
original region/body into the new IfOp region) and return the appropriate output
qubits, ensuring no zero-control CtrlOp is constructed (affects
ReplaceClassicalControls.cpp paths like replaceClassicalControlsOnlyControl and
replaceClassicalControlsTwoOfTwoControls).
- Around line 58-63: The isDiagonal predicate omits RzOp so controlled-rz paths
are not recognized; update the isDiagonal function (the dyn_cast<InvOp> branch
and the isa<> return) to include RzOp among the recognized diagonal ops (i.e.,
add RzOp to the isa<ZOp, SOp, TOp, POp, SdgOp, TdgOp, IdOp> list) so
controlled-`rz` will take the diagonal-swap path.

In
`@mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_replace_classical_controls.cpp`:
- Around line 377-380: Remove the leftover debug printing by deleting the
module->dump() call at the end of the test in
test_qco_replace_classical_controls.cpp; keep the existing assertions
runReplaceClassicalControlsPass(module.get()).succeeded() and
runCanonicalizerPass(reference.get()).succeeded() but remove the module->dump()
line so the test no longer prints the transformed module to CI logs.

---

Outside diff comments:
In `@mlir/include/mlir/Dialect/QCO/IR/QCOOps.td`:
- Around line 1071-1077: The QCO region wrapper ops declare the Pure trait which
expands to AlwaysSpeculatable + NoMemoryEffect and incorrectly
duplicates/overrides the region-modelled effects; remove the Pure trait from the
op definitions for CtrlOp, InvOp, and IfOp (the TD defs named CtrlOp, InvOp,
IfOp) so they only keep RecursiveMemoryEffects (and other existing traits like
UnitaryOpInterface, AttrSizedOperandSegments, AttrSizedResultSegments,
SameOperandsAndResultType, SameOperandsAndResultShape,
SingleBlockImplicitTerminator) to ensure effects are solely driven by the nested
region.

In `@mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp`:
- Around line 1105-1165: The finalize paths currently emit a func::ReturnOp
without validating or matching the main function's declared result types
(initialize(TypeRange) may set a non-i64 or different arity), so update
QCOProgramBuilder::finalize() and QCOProgramBuilder::finalize(ValueRange) to
first query mainFunc.getType().getResults(), assert that returnValues.size()
equals that result list size and that each returnValues[i].getType() ==
expectedResults[i], and if the no-arg finalize() is used produce appropriate
default constants per expectedResults type (not always an i64 zero) instead of
unconditionally calling intConstant(0); then create the func::ReturnOp with
validated/matched values (and emit a clear error via llvm::reportFatalUsageError
if arity/types mismatch). Ensure references: finalize(), finalize(ValueRange),
initialize(TypeRange), func::ReturnOp::create, mainFunc.getType().getResults(),
and intConstant are used when implementing the checks and per-type default
constant construction.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 404b26d5-75c2-4dfc-86eb-990ccae1574b

📥 Commits

Reviewing files that changed from the base of the PR and between e868fe9 and c1fda94.

📒 Files selected for processing (17)
  • CHANGELOG.md
  • mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h
  • mlir/include/mlir/Dialect/QCO/IR/QCODialect.td
  • mlir/include/mlir/Dialect/QCO/IR/QCOOps.td
  • mlir/include/mlir/Dialect/QCO/QCOUtils.h
  • mlir/include/mlir/Dialect/QCO/Transforms/Passes.td
  • mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp
  • mlir/lib/Dialect/QCO/IR/Operations/MeasureOp.cpp
  • mlir/lib/Dialect/QCO/IR/Operations/ResetOp.cpp
  • mlir/lib/Dialect/QCO/IR/QCOOps.cpp
  • mlir/lib/Dialect/QCO/IR/SCF/IfOp.cpp
  • mlir/lib/Dialect/QCO/Transforms/Optimizations/MeasurementLifting.cpp
  • mlir/lib/Dialect/QCO/Transforms/Optimizations/ReplaceClassicalControls.cpp
  • mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp
  • mlir/unittests/Dialect/QCO/Transforms/Optimizations/CMakeLists.txt
  • mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_measurement_lifting.cpp
  • mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_replace_classical_controls.cpp

Comment thread CHANGELOG.md Outdated
Comment thread mlir/include/mlir/Dialect/QCO/QCOUtils.h Outdated
Comment thread mlir/lib/Dialect/QCO/Transforms/Optimizations/ReplaceClassicalControls.cpp Outdated
@codecov

codecov Bot commented Jun 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.36620% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...nsforms/Optimizations/ReplaceClassicalControls.cpp 94.3% 4 Missing ⚠️

📢 Thoughts on this report? Let us know!

@DRovara DRovara mentioned this pull request Jun 3, 2026
11 tasks
@mergify mergify Bot added the conflict label Jun 3, 2026
@DRovara
DRovara force-pushed the mlir/replace-classical-controls branch from ad1be84 to febb216 Compare July 14, 2026 04:51
@mergify mergify Bot removed the conflict label Jul 14, 2026
@mergify mergify Bot added the conflict label Jul 15, 2026
@DRovara

DRovara commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_measurement_lifting.cpp`:
- Around line 534-574: Correct the expected module in
dontLiftMeasurementMultipleGatesInControl by keeping the control region before
measuring its control qubit, matching the transformed program’s operation order.
Update the referenceBuilder sequence so r0S0 is passed through ctrl first, then
measure the resulting control state, while preserving the multi-gate z/x control
body and final comparison.

In
`@mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_replace_classical_controls.cpp`:
- Line 137: Remove the const qualifier from the ValueRange parameters in all
five lambdas, including those in the QCO optimization test locations identified
by the symbols or surrounding lambda signatures. Keep the parameter types as
ValueRange and change only the signature qualifiers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 74274d8e-01d5-487e-9595-db52fcb37ce0

📥 Commits

Reviewing files that changed from the base of the PR and between e78c99a and 5e482a9.

📒 Files selected for processing (9)
  • CHANGELOG.md
  • mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h
  • mlir/include/mlir/Dialect/QCO/Transforms/Passes.td
  • mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp
  • mlir/lib/Dialect/QCO/Transforms/Optimizations/MeasurementLifting.cpp
  • mlir/lib/Dialect/QCO/Transforms/Optimizations/ReplaceClassicalControls.cpp
  • mlir/unittests/Dialect/QCO/Transforms/Optimizations/CMakeLists.txt
  • mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_measurement_lifting.cpp
  • mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_replace_classical_controls.cpp

@mergify mergify Bot removed the conflict label Jul 17, 2026
@mergify mergify Bot added the conflict label Jul 19, 2026
DRovara and others added 2 commits July 20, 2026 17:07
Signed-off-by: Damian Rovara <93778306+DRovara@users.noreply.github.com>
@DRovara
DRovara marked this pull request as ready for review July 20, 2026 15:12
@DRovara
DRovara requested a review from burgholzer July 20, 2026 15:13
@DRovara

DRovara commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@mergify mergify Bot removed the conflict label Jul 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c++ Anything related to C++ code feature New feature or request MLIR Anything related to MLIR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant